﻿using System;

using System.Collections.Generic;
using System.Text;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Drawing.Imaging;
using System.Diagnostics;
using Silvermoon.OpenGL.Native;

namespace Silvermoon.OpenGL
{
    /// <summary>
    /// Holds a <see cref="Font"/> in a texture for fast rendering in conjunction with <see cref="TextureString"/>.
    /// </summary>
    public class FontSprite : IDisposable
    {
        #region structs

        struct tagTextMetric
        {
            public int tmHeight;
            public int tmAscent;
            public int tmDescent;
            public int tmInternalLeading;
            public int tmExternalLeading;
            public int tmAveCharWidth;
            public int tmMaxCharWidth;
            public int tmWeight;
            public int tmOverhang;
            public int tmDigitizedAspectX;
            public int tmDigitizedAspectY;
            public byte tmFirstChar;
            public byte tmLastChar;
            public byte tmDefaultChar;
            public byte tmBreakChar;
            public byte tmItalic;
            public byte tmUnderlined;
            public byte tmStruckOut;
            public byte tmPitchAndFamily;
            public byte tmCharSet;
        };


        const int StartCharIndex = 0;
        const int EndCharIndex = 255;

        #endregion
        #region ctor/dtor

        static FontSprite()
        {
            StringBuilder sb = new StringBuilder();
            for (int c = 32; c < 255; c++)
            {
                sb.Append((char)c);
            }
            sb.Append("€√±≠≤≥÷∞µ←");
            DefaultCharacters = sb.ToString();
        }

        public FontSprite(Font font) : this(font, DefaultCharacters) { }

        public FontSprite(Font font, string charMap)
            : base()
        {
            charMap = " " + charMap;
            this.Font = font;
            IntPtr hfont = font.ToHfont();
            IntPtr hdc = fontGraphics.GetHdc();
            tagTextMetric metrics = new tagTextMetric();

            int[] charWidths = new int[EndCharIndex - StartCharIndex + 1];
            int[] charWidths2 = new int[4];
            try
            {
                SelectObject(hdc, hfont);
                if (!GetCharWidth32(hdc, StartCharIndex, EndCharIndex, charWidths)) throw new SystemException("Failure while measuring character widths.");


                foreach (char c in charMap)
                {
                    if (this.charWidths.ContainsKey(c)) continue;
                    if (c >= StartCharIndex && c <= EndCharIndex)
                    {
                        this.charWidths.Add(c, charWidths[c]);
                    }
                    else
                    {
                        if (!GetCharWidth32(hdc, (int)c, ((int)c) + 0, charWidths2)) throw new SystemException("Failure while measuring character widths.");
                        this.charWidths.Add(c, charWidths2[0]);
                    }
                }
                GetTextMetrics(hdc, ref metrics);
            }
            finally
            {
                fontGraphics.ReleaseHdc(hdc);
            }

            CharHeight = metrics.tmHeight;
            MaxCharWidth = metrics.tmMaxCharWidth;
            TrailingSpace = metrics.tmExternalLeading;
            LeadingSpace = metrics.tmInternalLeading;

            CreateTextureArrayPointer();
            texture = new Texture();
            RenderBitmapAndFillCharArray(texture);
        }

        public void Dispose()
        {
            if (texture != null) texture.Dispose();
            texture = null;
            textureArrayPointer = null;
            charWidths = null;
        }

        #endregion
        #region fields

        private static FontSprite defaultFont;
        private static FontSprite defaultBoldFont;
        private int elipsisWidth = 0;

        /// <summary>
        /// Specifies all characters that are used for new fonts by default if not explicitely specified in the constructor.
        /// </summary>
        public static string DefaultCharacters;

        private readonly static Bitmap fontBitmap = new Bitmap(1, 1, PixelFormat.Format16bppRgb565);
        private readonly static Graphics fontGraphics = Graphics.FromImage(fontBitmap);
        private Texture texture;

        private short[] textureArrayPointer;

        private Dictionary<char, int> charWidths = new Dictionary<char, int>();
        private Dictionary<char, int> charIndex = new Dictionary<char, int>();

        #endregion
        #region props

        /// <summary>
        /// Gets the width of the elipsis aka "..." characters.
        /// </summary>
        public int ElipsisWidth
        {
            get
            {
                if (elipsisWidth == 0) elipsisWidth = MeasureString("...").Width;
                return elipsisWidth;
            }
        }
        public static float DefaultFontHeight = 9f;

        public static FontSprite Regular
        {
            get
            {
                if (defaultFont == null) defaultFont = FromFont(new Font(FontFamily.GenericSerif, DefaultFontHeight, FontStyle.Regular));
                return defaultFont;
            }
        }

        public static FontSprite Bold
        {
            get
            {
                if (defaultBoldFont == null) defaultBoldFont = FromFont(new Font(FontFamily.GenericSerif, DefaultFontHeight, FontStyle.Bold));
                return defaultBoldFont;
            }
        }

        public Size TextureSize { get; private set; }
        public Font Font { get; private set; }
        public int CharHeight { get; private set; }
        public int MaxCharWidth { get; private set; }

        internal int TrailingSpace { get; private set; }
        internal int LeadingSpace { get; private set; }

        public Texture Texture { get { return texture; } }

        /// <summary>
        /// Gets triples of left/top/width values for each character.
        /// </summary>
        public short[] TextureArrayPointer { get { return textureArrayPointer; } }

        #endregion
        #region methods
        [DllImport("coredll")]
        extern static bool GetCharWidth32(IntPtr hdc, int iFirstChar, int iLastChar, [Out] int[] lpBuffer);

        [DllImport("coredll")]
        extern static IntPtr SelectObject(IntPtr hdc, IntPtr obj);

        [DllImport("coredll")]
        extern static bool GetTextMetrics(IntPtr hdc, ref tagTextMetric lptm);


        public Size MeasureString(string text, int charSpace, int maxChars)
        {
            if (text == null) text = string.Empty;
            int x = 0;
            foreach (char c in text)
            {
                if (--maxChars < 0) break;
                x += CharacterWidth(c) + charSpace;
            }
            return new Size(x, CharHeight);
        }

        public int CharacterWidth(char c)
        {
            int w;
            if (!charWidths.TryGetValue(c, out w)) return 0;
            return w;
        }

        public Size MeasureString(string text)
        {
            return MeasureString(text, 0, int.MaxValue);
        }

        private int GetTotalWidth()
        {

            int trail = TrailingSpace + LeadingSpace;
            int sum = 0;
            foreach (int w in charWidths.Values) sum += w + trail;

            return sum;
        }

        private int CalculateIdealBitmapWidth()
        {
            int total = GetTotalWidth();
            int ideal = (int)Math.Ceiling(Math.Sqrt(total * CharHeight));

            return Texture.ConvertToValidTextureDimension(ideal);
        }

        private int CalculateIdealBitmapHeight(int width)
        {
            int charHeight = CharHeight;
            int x = 0;
            int y = 0;

            int trail = LeadingSpace + TrailingSpace;
            width -= trail;
            for (int i = StartCharIndex; i <= EndCharIndex; i++)
            {
                int charWidth = CharacterWidth((char)i);
                if ((x + charWidth) >= width)
                {
                    x = 0;
                    y += charHeight;
                }

                x += charWidth + trail;
            }
            return Texture.ConvertToValidTextureDimension(y);
        }

        /// <summary>
        /// Generate a bitmap that contains an array of characters with a dimension of CharsPerRow * Rows.
        /// Each character is separated by MaxCharWidth.
        /// </summary>
        /// <returns>A bitmap with a character matrix.</returns>
        private void RenderBitmapAndFillCharArray(Texture texture)
        {
            int bmWidth = CalculateIdealBitmapWidth();
            int bmHeight = CalculateIdealBitmapHeight(bmWidth);
            int charHeight = CharHeight;

            TextureSize = new Size(bmWidth, bmHeight);

            float fWidth = (float)bmWidth;
            float fHeight = (float)bmHeight;

            int trail = TrailingSpace + LeadingSpace;
            using (Graphics g = texture.CreateGraphics(bmWidth, bmHeight, GlesPixelFormat.Alpha, Color.Black))
            {
                g.Clear(Color.Black);
                using (SolidBrush brush = new SolidBrush(Color.White))
                {
                    int x = 0;
                    int y = 0;
                    int idx = 0;
                    foreach (char c in charWidths.Keys)
                    {
                        charIndex.Add(c, idx);
                        int i = (int)c;
                        int charWidth = CharacterWidth(c);
                        if (charWidth > 0)
                        {
                            if ((x + charWidth) >= bmWidth)
                            {
                                x = 0;
                                y += charHeight;
                            }
                            g.DrawString(c.ToString(), Font, brush, x, y);
                        }

                        AddCharacterArray(idx++, x, y, charWidth);
                        x += charWidth + trail;
                    }
                }
            }
            texture.ReleaseGraphics();
        }


        /// <summary>
        /// Creates the triangle pointers for each character.
        /// Every character has 2 triangles, with a height of CharHeight/bitmap.Height and a width of CharWidth/bitmap.Width.
        /// this is because the complete texture must have a size of 1;1 to fill the complete vertex.
        /// </summary>
        private void CreateTextureArrayPointer()
        {
            int size = charWidths.Count * 3;
            textureArrayPointer = new short[size];
        }

        void AddCharacterArray(int charIndex, int x, int y, int width)
        {
            charIndex *= 3;
            short[] a = textureArrayPointer;
            a[charIndex + 0] = (short)x;
            a[charIndex + 1] = (short)y;
            a[charIndex + 2] = (short)width;
        }

        ///// <summary>
        ///// Adds 4 pairs of x/y to the textureArrayPointer array that describe the
        ///// bottom/left - top/left - bottom/right - top/right of the character with the specified index in the bitmap.
        ///// </summary>
        ///// <param name="index">The character index</param>
        //private void AddCharacterArray(int charIndex, float left, float top, float right, float bottom)
        //{
        //    int aIndex = charIndex * 8;
        //    float[] a = textureArrayPointer;
        //    a[aIndex + 0] = left;
        //    a[aIndex + 1] = bottom;

        //    a[aIndex + 2] = left;
        //    a[aIndex + 3] = top;

        //    a[aIndex + 4] = right;
        //    a[aIndex + 5] = bottom;

        //    a[aIndex + 6] = right;
        //    a[aIndex + 7] = top;
        //}


        #endregion
        #region statics

        private static Dictionary<Font, FontSprite> glFonts = new Dictionary<Font, FontSprite>();

        public static FontSprite FromFont(Font font)
        {
            FontSprite result;
            if (glFonts.TryGetValue(font, out result)) return result;

            result = new FontSprite(font);
            glFonts.Add(font, result);
            return result;
        }


        #endregion

        internal int GetCharIndex(char c)
        {
            int n;
            if (!charIndex.TryGetValue(c, out n)) return 0;  // space
            return n;
        }
    }
}
